#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# bsd64 -- bsddb with base64-codec
#

#
# This program is free software; you can redistribute it and/or modify  
# it under the terms of the GNU General Public License as published by  
# the Free Software Foundation; either version 2 of the License, or     
# (at your option) any later version.                                   
#                                                                         
# A copy of the license can be found in the license.txt file supplied   
# with this software or at: http://www.gnu.org/copyleft/gpl.html       
#


import os,os.path,tempfile,fnmatch
import base64, time, re
import traceback

from cStringIO import StringIO

from bsddb import db

import mmap

try :
    from os import SEEK_SET,SEEK_END
except :
    SEEK_SET = 0
    SEEK_END = 2

from celtuce.help import logging


class   Bsd64DB(object) :

    INDEX_FILE = 'index.db'
    INDEX_FILENAME_PREFIX = 'index'
    SLICE_FILENAME_PREFIX = 'data'
    WORK_FILE  = '.'.join((SLICE_FILENAME_PREFIX,'0'))

    OP_INSERT = 0
    OP_DELETE = 9

    OP_INSERT_STR = str(OP_INSERT)
    OP_DELETE_STR = str(OP_DELETE)

    SLICE_MINOR_COMPACT_SIZE = 1024 * 1024 * 512
    SLICE_NAME_PATTERN = re.compile(r'^data\.(\d+)$', re.UNICODE)
    INDEX_NAME_PATTERN = re.compile(r'^index\.(\d+)$', re.UNICODE)

    SPLIT_NAME_PATTERN = re.compile(r'^split\.(\d+)$', re.UNICODE)

    ERASE_DATA = base64.b64encode('D')

    DELETED_LOG = 'deleted.log'

    # maximum size of data returned for find()
    MAX_FIND_DATA_SIZE = 1024 * 1024 * 4

    
    def __init__(self, db_path) :
        self.table_dir = db_path
        if not os.path.exists(self.table_dir) and not os.path.lexists(self.table_dir) :
            os.makedirs(self.table_dir, 0755)

        self.table_index_db = None
        self.table_index_cursor = None

        self.fp_work_path = os.sep.join((self.table_dir, self.WORK_FILE))
        self.fp_work = None
        self.fps_readonly = {}

        # record buffer
        self._fp_record = StringIO()

        # count of slice files
        self._number_slice_files = 0
        self._approx_total_size = 0

        # deleted logs
        self.deleted_log = os.sep.join((self.table_dir, self.DELETED_LOG))
        self._fp_deleted_log = None

        self._logger = logging.getLogger('celtuce')
        ## __init__()

    def __del__(self) :
        self.shutdown()
        ## __del__()

    def shutdown(self) :
        if self.table_index_db is not None :
            try :
                self.table_index_db.sync()
                self.table_index_db.close()
                self.table_index_db = None
            except :
                self._logger.excpt()

        if self.fp_work is not None :
            self.fp_work.close()
            self.fp_work = None

        for fno,fp in self.fps_readonly.iteritems() :
            fp.close()
        self.fps_readonly = {}

        if self._fp_record :
            self._fp_record.close()
            self._fp_record = None

        if self._fp_deleted_log :
            self._fp_deleted_log.close()
            self._fp_deleted_log = None
        ## shutdown()

    def getApproxVolume(self) :
        if self._approx_total_size < 1 :
            if self._number_slice_files < 1 :
                self._number_slice_files = self.__count_slice_files()
            self._approx_total_size = self._number_slice_files * self.SLICE_MINOR_COMPACT_SIZE
        return self._approx_total_size
        ## getApproxVolume()

    def isCompactable(self) :
        return False
        ## isCompactable()

    def compact(self) :
        raise NotImplementedError
        ## compact()

    ####------------------------------------------------------------####

    def __init_db(self) :
        if not self.table_index_db :
            table_index_path = os.sep.join((self.table_dir, self.INDEX_FILE))
            
            dbtype       = db.DB_BTREE  
            dbopenflags  = 0 
            dbsetflags   = db.DB_DUPSORT
            dbmode       = 0664
            dbname       = None

            self.table_index_db = None
            self.table_index_db = db.DB()
            self.table_index_db.set_flags(dbsetflags)

            self.table_index_db.open(table_index_path, mode = dbmode, dbtype = dbtype, flags = dbopenflags|db.DB_CREATE)

            self.table_index_cursor = self.table_index_db.cursor()

        if not self.fp_work :
            self.fp_work = file(self.fp_work_path, 'a', 1)

        return True
        ## __init_db()

    def __getTimestamp(self) :
        return int(time.time() * 1000)
        ## __getTimestamp()

    def __encodeIndexData(self, data_iterable) :
        data = [ str(x) for x in data_iterable ]
        return ','.join(data)
        ## __encodeIndexData()

    def __decodeIndexData(self, data) :
        data_list = data.split(',')
        return [ int(x) for x in data_list ]
        ## __decodeIndexData()

    ####------------------------------------------------------------####

    def insert(self, key, data, timestamp_ms) :
        if not key or not data :
            return True
        if not self.__init_db() :
            return False

        if not timestamp_ms :
            timestamp_ms = self.__getTimestamp()

        if self.__existKeyWithTimestamp(key, timestamp_ms) :
            # key with the same timestamp exists
            return True

        return self.__doInsert(key, data, timestamp_ms)
        ## insert()

    def __existKeyWithTimestamp(self, key, timestamp_ms) :
        #c = self.table_index_db.cursor()
        c = self.table_index_cursor
        try :
            r = c.set( key )
            if not r :  
                return False
            while r :
                _key,_data = r
                _ts,_op,_fileno,_offset,_length = self.__decodeIndexData( _data )
                if _ts == timestamp_ms :
                    return True
                r = c.next()
        except :
            self._logger.excpt()
            return False
        finally :
            #c.close()
            c = None

        return False
        ## __existKeyWithTimestamp()

    def __doInsert(self, key, data, timestamp_ms) :
        if not timestamp_ms :
            timestamp_ms = self.__getTimestamp()

        data_b64 = base64.b64encode(data)

        # STEP. append data to file
        self._fp_record.truncate(0)
        self._fp_record.write(key)
        self._fp_record.write("\t")
        self._fp_record.write(self.OP_INSERT_STR)
        self._fp_record.write("\t")
        self._fp_record.write(str(timestamp_ms))
        self._fp_record.write("\t")
        self._fp_record.write(data_b64)
        value = self._fp_record.getvalue()

        self.fp_work.seek(0, SEEK_END)
        offset = self.fp_work.tell()
        length = len(value)
        self.fp_work.write(value)
        self.fp_work.write("\n")


        # STEP. update index
        data = self.__encodeIndexData( (timestamp_ms,self.OP_INSERT,0,offset,length) )
        try :
            self.table_index_db.put(key, data)
        except :
            self._logger.excpt()
            return False

        if (offset + length) >= self.SLICE_MINOR_COMPACT_SIZE :
            self.__minor_compact()

        return True
        ## __doInsert()


    ####------------------------------------------------------------####

    def find(self, key, timestamp_ms=0) :
        '''
        @param key : key of record
        
        @return 4-tuple(key,timestamp,op,data)
        '''
        if not key :
            return (key,0,0,None)
        if not self.__init_db() :
            return (key,0,0,None)

        time_start = time.time()
        row = self.__findone_in_index(key, timestamp_ms)
        time_last = time.time() - time_start
        print 'findone last %.2f seconds' % time_last
        if not row :
            return (key, 0, 0, None)
        ts,op,fileno,offset,length = row
        if op == self.OP_DELETE :
            return (key,ts,op,None)

        data = self.__read_data(fileno,offset,length)
        return (key,ts,op,data)
        ## find()

    def __findone_in_index(self, key, timestamp_ms=0) :
        '''
        @param key : key of record
        @param timestamp_ms : key occurs before this timestamp will be return

        @return tuple (ts,op,fileno,offset,length)
        '''
        found_row = None
        #c = self.table_index_db.cursor()
        c = self.table_index_cursor
        try :
            r = c.set( key )
            if not r :  
                return False
            while r :
                _key,_data = r
                # row = (timestamp, op, fileno, offset, length)
                row = self.__decodeIndexData( _data )
                if (timestamp_ms > 0) and row[0] > timestamp_ms :
                    r = c.next()
                    continue

                if found_row :
                    if row[:2] > found_row[:2] :
                        # timestamp means newly inserted
                        found_row = row
                else :
                    found_row = row
                r = c.next()
        except :
            self._logger.excpt()
            return False
        finally :
            #c.close()
            c = None

        return found_row
        ## __findone_in_index()


    ####------------------------------------------------------------####

    def __read_data_FILE(self, fileno, offset, length) :
        if fileno < 0 :
            return None

        if fileno in self.fps_readonly :
            _fp = self.fps_readonly[fileno]
        else :
            _fp = self.__open_slice_file(fileno)
            self.fps_readonly[fileno] = _fp

        _fp.seek(offset)
        data = _fp.read(length)
        if len(data) != length :
            raise RuntimeError,'fileno=%d offset=%d length=%d (length-got=%d)' % (fileno,offset,length,len(data))

        _key,_op,_ts,_data = data.split("\t",3)
        data = base64.b64decode(_data)

        return data.strip()
        ## __read_data()

    def __read_data_MMAP(self, fileno, offset, length) :
        if fileno < 0 :
            return None

        if fileno in self.fps_readonly :
            _fp = self.fps_readonly[fileno]
        else :
            _fp = self.__open_slice_file(fileno)
            self.fps_readonly[fileno] = _fp

        _mem = mmap.mmap(_fp.fileno(), offset + length, mmap.MAP_PRIVATE, mmap.PROT_READ, 0)
        data = _mem[offset : offset + length]
        _mem.close()
        if len(data) != length :
            raise RuntimeError,'fileno=%d offset=%d length=%d (length-got=%d)' % (fileno,offset,length,len(data))

        _key,_op,_ts,_data = data.split("\t",3)
        data = base64.b64decode(_data)

        return data.strip()
        ## __read_data()

    __read_data = __read_data_FILE

    def __open_slice_file(self, fileno) :
        filename  = '.'.join((self.SLICE_FILENAME_PREFIX,str(fileno)))
        filepath = os.sep.join((self.table_dir, filename))
        fp = file(filepath, 'r')
        return fp
        ## __open_slice_file()



    ####------------------------------------------------------------####

    def __minor_compact(self) :
        raise NotImplementedError
        ## __minor_compact()

    ## class Bsd64DB

